package org.sample.springbootasync.config;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.List;

import javax.net.ssl.SSLContext;
import javax.servlet.http.HttpServletRequest;

import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustAllStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.MediaType;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Configuration
public class RestTemplateConfig {

	@Value("${http.connection.connect-timeout-milliseconds:0}")
	private int connectTimeoutMilliseconds;
	
	@Value("${http.connection.read-timeout-milliseconds:0}")
	private int readTimeoutMilliseconds;
	
	@Value("${hhttp.client.cookie-management.enabled:true}")
	private boolean enableCookie;
	
	@Value("${http.client.ssl.verify-certificate:true}")
	private boolean enableVerifyCertificate;
	
	@Value("${http.client.ssl.trust-store}")
	private Resource keyStore;
	
	@Value("${http.client.ssl.trust-store-password}")
	private String keyStorePassword;
	
	@Value("${http.client.ssl.verify-host-name:true}")
	private boolean enableVerifyHostName;
	
	@Bean
	public RestTemplate restTemplate(RestTemplateBuilder builder){
		RestTemplate restTemplate = builder.build(); 
		addMessageConverterWithNewMediaType(restTemplate);
		restTemplate.setRequestFactory(clientHttpRequestFactory());
		restTemplate.getInterceptors().add(new RestInvokeHeaderInterceptor());
		restTemplate.getInterceptors().add(new RequestResponseLoggingInterceptor());
		return restTemplate;
	}
	
	private ClientHttpRequestFactory clientHttpRequestFactory() {

		HttpClientBuilder httpClientBuilder = HttpClients.custom();

		SSLContext sslContext;
		if (enableVerifyCertificate) {
			
			if (keyStore.exists()) {				
				try {
					sslContext = SSLContexts.custom()
							.loadTrustMaterial(keyStore.getFile(), keyStorePassword.toCharArray())
							.build();
				} catch (GeneralSecurityException | IOException e) {
					throw new IllegalStateException("Load SSL trust material failed.", e);
				}
			} else {
				sslContext = SSLContexts.createDefault();
			}
			
		} else {
			try {
				sslContext = SSLContexts.custom()
						.loadTrustMaterial(null, TrustAllStrategy.INSTANCE)
						.build();
			} catch (GeneralSecurityException e) {
				throw new IllegalStateException("Set SSL trust all material failed.", e);
			}
		}
		
		SSLConnectionSocketFactory socketFactory;
		if (enableVerifyHostName) {
			socketFactory = new SSLConnectionSocketFactory(sslContext);
		} else {
			socketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
		}
		httpClientBuilder.setSSLSocketFactory(socketFactory);

		if (!enableCookie) {
			httpClientBuilder.disableCookieManagement();
		}

		HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory(
				httpClientBuilder.build());

		httpRequestFactory.setReadTimeout(readTimeoutMilliseconds);
		httpRequestFactory.setConnectTimeout(connectTimeoutMilliseconds);
		

		return new BufferingClientHttpRequestFactory(httpRequestFactory);
	}
	
	/**
	 *  the interceptor to add header for rest client call.
	 *
	 */
	private static class RestInvokeHeaderInterceptor implements ClientHttpRequestInterceptor {
		
		@Override
		public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
				throws IOException {
			HttpHeaders headers = request.getHeaders();
			RequestAttributes ra = RequestContextHolder.getRequestAttributes();
			if(ra != null){
				ServletRequestAttributes sra = (ServletRequestAttributes) ra;
				HttpServletRequest webRequest = sra.getRequest();
				//headers.add(HttpHeaders.COOKIE, webRequest.getHeader(HttpHeaders.COOKIE))
				//headers.add("User-Session-Id", webRequest.getHeader("User-Session-Id"));
				if(StringUtils.isEmpty(headers.getFirst("Access-Channel"))){
					headers.add("Access-Channel", webRequest.getHeader("Access-Channel"));
				}
			}
			return execution.execute(request, body);
		}
	}
	/**
	 *  the interceptor to add header for rest client call.
	 *
	 */
	private static class RequestResponseLoggingInterceptor implements ClientHttpRequestInterceptor {

		private static final Logger LOGGER = LoggerFactory.getLogger(RequestResponseLoggingInterceptor.class);

		@Override
		public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
				throws IOException {

			String requestBody = new String(body, StandardCharsets.UTF_8);
			LOGGER.info("HTTP {} request to: {}", request.getMethod().name(), request.getURI());
			if (!StringUtils.isEmpty(requestBody)) {
				LOGGER.info(requestBody);
			}

			ClientHttpResponse response = execution.execute(request, body);

			String responseBody = StreamUtils.copyToString(response.getBody(), Charset.defaultCharset());
			LOGGER.info("HTTP {} response from: {}", request.getMethod().name(), request.getURI());
			if (!StringUtils.isEmpty(responseBody)) {
				LOGGER.info(responseBody);
			}
			
			return response;
		}
	}
	
	/**
	 * add new HttpMessageConverter with new MediaType eg. application/octet-stream
	 * 
	 * @param restTemplate
	 */
	private void addMessageConverterWithNewMediaType(RestTemplate restTemplate) {
		List<HttpMessageConverter<?>> httpMessageConverters = restTemplate.getMessageConverters();
		boolean mediaTypeExist = false;
		for(HttpMessageConverter<?> httpMessageConverter : httpMessageConverters) {
			if(httpMessageConverter instanceof MappingJackson2HttpMessageConverter 
					&& httpMessageConverter.getSupportedMediaTypes().contains(MediaType.APPLICATION_OCTET_STREAM)) {
				mediaTypeExist = true;
				break;
			}
		}
		if(!mediaTypeExist) {
			MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
			List<MediaType> mediaTypes = new ArrayList<>();
			mediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
			converter.setSupportedMediaTypes(mediaTypes);	
			restTemplate.getMessageConverters().add(converter);
		}
	}
	
}
